package erwiki.engine.internal.services;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import javax.servlet.ServletContext;

import org.apache.log4j.Logger;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.annotation.NonNull;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ServiceScope;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;

import erwiki.api.Release;
import erwiki.api.WikiPartReference;
import erwiki.api.component.IPartPreferences;
import erwiki.api.component.WikiComponent;
import erwiki.api.core.Engine;
import erwiki.api.core.WikiSession;
import erwiki.api.event.EngineEvent;
import erwiki.api.exceptions.InternalWikiException;
import erwiki.api.exceptions.ProviderException;
import erwiki.api.exceptions.WikiException;
import erwiki.api.filters.FilterManager;
import erwiki.api.rss.RssGenerator;
import erwiki.api.search.SearchManager;
import erwiki.api.ui.CommandResolver;
import erwiki.api.url.URLConstructor;
import erwiki.configuration.IWikiConfiguration;
import erwiki.util.FileUtil;

//@formatter:off
@Component(
	name = "erwiki.Engine",
	service = Engine.class,
	scope = ServiceScope.SINGLETON,
	immediate = true)
//@formatter:on
public class WikiEngine implements Engine, IPartPreferences {

	private static final Logger log = Logger.getLogger(WikiEngine.class);

	// -- OSGi service handling ----------------------(start)--

	private BundleContext bundleContext;

	/** Customized ServiceTracker of {@link WikiSession} component. */
	private ServiceTracker<?, ?> sessionServiceTracker;

	@Reference
	EventAdmin eventAdmin;

	/** Stores ElWiki configuration. */
	@Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC)
	private IWikiConfiguration wikiConfiguration;

	@Activate
	protected void startupService(ComponentContext componentContext) throws WikiException {
		this.bundleContext = componentContext.getBundleContext();
		{////////////////////////////////////////
//		IEclipseContext serviceContext = EclipseContextFactory.getServiceContext(bundleContext);
//		ContextInjectionFactory.inject(this, serviceContext);
		}
		createServiceTracker(bundleContext);

		ServiceReference<?>[] refs = null;
		try {
			refs = bundleContext.getAllServiceReferences(WikiComponent.class.getName(), null);
		} catch (InvalidSyntaxException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}

		int counterOfRegistered = 0;
		int counterOfFounded = 0;
		for (ServiceReference<?> ref : refs) {
			counterOfFounded++;
			@SuppressWarnings("unchecked")
			WikiComponent service = bundleContext.getService((ServiceReference<WikiComponent>) ref);
			// TODO: здесь (service == null) если сервис не инициализировался, при ошибках в нем.
			Class<?> clazz = service.getClass();

			Class<?>[] ifaces = clazz.getInterfaces();
			boolean isWikiComponent = Arrays.stream(ifaces).anyMatch(WikiComponent.class::equals);
			if (isWikiComponent) {
				Class<?> ifaceComponent = ifaces[0]; // workaround!
				components.put(ifaceComponent, service);
				// log.debug(" ~~ clazz: " + clazz.getSimpleName() + " instance of " +
				// ifaceComponent.getSimpleName());
				counterOfRegistered++;
			}
		}
		log.debug(" ~~ Registered (" + counterOfRegistered + ") ElWiki components. From founded: " + counterOfFounded);

		/* Processing the @WikiPartReference annotation for ErWiki components fields.
		 */
		for (Object serviceInstance : components.values()) {
			initialiseReferencedFields(serviceInstance);
		}

		try {
			initialize();
			log.info("Root path for this Wiki is: '" + m_rootPath + "'");
		} catch (final Exception e) {
			log.error("Failer WikiEngine activate. Intialization stage errored.", e);
			// :FVK: final String msg = Release.APPNAME + ": Unable to initialise service. " + e.getMessage();
			// throw new WikiException(msg, e);
		}
	}

	@Deactivate
	protected void shutdown() {
		//
	}

	/**
	 * Creates ServiceTracker for tracking component of Session type.
	 *
	 * @param bundleContext
	 */
	private void createServiceTracker(BundleContext bundleContext) {
		Filter filter = null;
		try {
			filter = bundleContext.createFilter(
					"(&(" + org.osgi.framework.Constants.OBJECTCLASS + "=" + WikiSession.class.getName() + "))");
		} catch (InvalidSyntaxException e) {
			// should never happen.
		}
		ServiceTrackerCustomizer<WikiSession, ServiceReference<WikiSession>> stCustomizer = createStCustomizer();
		this.sessionServiceTracker = new ServiceTracker<WikiSession, ServiceReference<WikiSession>>(bundleContext,
				filter, stCustomizer);
		this.sessionServiceTracker.open(true);
	}

	/**
	 * Returns customized ServiceTracker for tracking component of Session type.
	 *
	 * @return ServiceTracker for tracking component of Session type.
	 */
	private ServiceTrackerCustomizer<WikiSession, ServiceReference<WikiSession>> createStCustomizer() {
		return new ServiceTrackerCustomizer<WikiSession, ServiceReference<WikiSession>>() {

			@Override
			public ServiceReference<WikiSession> addingService(ServiceReference<WikiSession> serviceRef) {
				// for(String key:reference.getPropertyKeys()) {}
				log.debug("~~ ~~ adding service: " + serviceRef);
				WikiSession serviceInstance = bundleContext.getService(serviceRef);
				initialiseReferencedFields(serviceInstance);
				return null;
			}

			@Override
			public void modifiedService(ServiceReference<WikiSession> reference,
					ServiceReference<WikiSession> service) {
				log.debug("~~ ~~ modified service");
			}

			@Override
			public void removedService(ServiceReference<WikiSession> reference, ServiceReference<WikiSession> service) {
				log.debug("~~ ~~ removed service");
			}
		};
	}

	// -- OSGi service handling ------------------------(end)--

	/**
	 * Property name for where the ErWiki work directory should be.</br>
	 * If not specified, sets to workspace place.
	 */
	private static final String SYSTEM_PROP_WORK_DIR = "erwiki.workDir";

	/**
	 * Property name for where the H2 database should be located.</br>
	 * If not specified, sets to area in the workspace place - ${workspace_loc}/.
	 */
	private static final String SYSTEM_PROP_H2_DATABASE_PLACE = "erwiki.h2.database.place";

	/**
	 * Store the file path to the basic URL. When we're not running as a servlet, it defaults to the
	 * user's current directory.
	 */
	private String m_rootPath = System.getProperty("user.dir");

	/**
	 * Store the ServletContext that we're in. This may be null if WikiEngine is not running inside a
	 * servlet container (i.e. when testing).
	 */
	// TODO: этот код устарел (в ElWiki инициализация относительно OSGi, а не сервлета.
	// ServletContext==null).
	private ServletContext m_servletContext = null;

	/** The time when this engine was started. */
	private Date m_startTime;

	/** engine is up and running or not */
	private boolean m_isConfigured = false;

	/** Stores wikiengine attributes. */
	private Map<String, Object> m_attributes = new ConcurrentHashMap<>();

	/** Stores WikiEngine's associated managers- <interface, instance>. */
	protected Map<Class<?>, Object> components = new ConcurrentHashMap<>() {
		private static final long serialVersionUID = 8475550897265370262L;
		{
			put(Engine.class, WikiEngine.this);
		}
	};

	/** If true, uses UTF8 encoding for all data */
	private boolean isUtf8Encoding;

	private final IPath pathWorkspace = ResourcesPlugin.getWorkspace().getRoot().getLocation();
	private IPath pathWorkDir;
	private IPath dbStorePath;

	// =========================================================================

	/**
	 * Does all the real initialization.
	 */
	private void initialize() throws WikiException {
		m_startTime = new Date();
		log.info("** initialization ** WikiEngine **");
		log.info(Release.APPNAME + " " + Release.getVersionString() + " starting. Whee!");

		log.debug("Java version: " + System.getProperty("java.runtime.version"));
		log.debug("Java vendor: " + System.getProperty("java.vm.vendor"));
		log.debug("OS: " + System.getProperty("os.name") + " " + System.getProperty("os.version") + " "
				+ System.getProperty("os.arch"));
		log.debug("  Default server locale: " + Locale.getDefault());
		log.debug("Default server timezone: " + TimeZone.getDefault().getDisplayName(true, TimeZone.LONG));

		/* Определение рабочего каталога, место размещения H2 DB. */
		pathWorkDir = Optional.ofNullable(FileUtil.getDirectoryPlace(SYSTEM_PROP_WORK_DIR)).orElse(pathWorkspace);
		dbStorePath = Optional.ofNullable(FileUtil.getDirectoryPlace(SYSTEM_PROP_H2_DATABASE_PLACE))
				.orElse(pathWorkspace);
		log.info(" Working directory is: '" + pathWorkDir + "'");
		log.info("       H2 location is: '" + dbStorePath + "'");

		if (m_servletContext != null) {
			// TODO: этот код устарел (в ElWiki инициализация относительно OSGi, а не сервлета.
			// ServletContext==null).
			log.info("Servlet container: " + m_servletContext.getServerInfo());
			if (m_servletContext.getMajorVersion() < 3
					|| (m_servletContext.getMajorVersion() == 3 && m_servletContext.getMinorVersion() < 1)) {
				throw new InternalWikiException(
						"JSPWiki requires a container which supports at least version 3.1 of Servlet specification");
			}
		}

		log.debug("◄►initialization◄► STAGE ONE.");

		// workaround - added flag "isUtf8Encoding".
		GlobalPreferences globalPrefs = getManager(GlobalPreferences.class);
		isUtf8Encoding = StandardCharsets.UTF_8.name()
				.equalsIgnoreCase(getPreference(GlobalPreferences.Prefs.PROP_ENCODING, String.class));

		eventAdmin.sendEvent(new Event(EngineEvent.Topic.INIT_STAGE_ONE, Collections.emptyMap()));

		Set<Object> components = new HashSet<>(this.components.values());
		for (Object componentInstance : components) {
			if (componentInstance instanceof WikiComponent wikiComponent) {
				try {
					wikiComponent.initialize();
				} catch (Exception e) {
					log.error("Failed intialization of " + componentInstance.getClass().getSimpleName(), e);
				}
			}
		}

		log.debug("◄►initialization◄► STAGE TWO.");
		eventAdmin.sendEvent(new Event(EngineEvent.Topic.INIT_STAGE_TWO, Collections.emptyMap()));

		//
		// Initialize the important modules. Any exception thrown by the managers means that we will not
		// start up.
		//
		try {
			// Hook the different manager routines into the system.
			FilterManager filterManager = this.getManager(FilterManager.class);
			// :FVK: -- оставлено для примера:
			// filterManager.addPageFilter(this.getManager(ReferenceManager.class), -1001);
			filterManager.addPageFilter(this.getManager(SearchManager.class), -1002);
		} catch (RuntimeException e) {
			// RuntimeExceptions may occur here, even if they shouldn't.
			log.error("Failed to add filters.", e);
		} catch (Exception e) {
			// Final catch-all for everything
			log.error("ErWiki failed to add filters, due to an unknown exception.", e);
		}

		log.info("◄►initialization◄► DONE.");
		m_isConfigured = true;
	}

	/** {@inheritDoc} */
	@Override
	public void initialiseReferencedFields(Object instance) {
		for (Field field : instance.getClass().getDeclaredFields()) {
			if (field.isAnnotationPresent(WikiPartReference.class)) {
				try {
					Class<?> typeField = field.getType();
					Object targetService = components.get(typeField);
					field.setAccessible(true);
					field.set(instance, targetService);
				} catch (Exception e) {
					log.fatal("Failed reflection action", e);
				}
			}
		}
	}

	@Override
	public IPath getWorkDir() {
		return pathWorkDir;
	}

	@Override
	public IPath getDbPlace() {
		return dbStorePath;
	}

	@Override
	public IPath getWorkspacePath() {
		return pathWorkspace;
	}

	@Override
	public String getApplicationName() {
		return getPreference(GlobalPreferences.Prefs.APP_NAME, String.class);
	}

	/** {@inheritDoc} */
	@Override
	@Deprecated
	public boolean isConfigured() {
		return m_isConfigured;
	}

	/** {@inheritDoc} */
	@Override
	public Date getStartTime() {
		return (Date) m_startTime.clone();
	}

	/** {@inheritDoc} */
	@Override
	public String getGlobalRSSURL() {
		final RssGenerator rssGenerator = this.getManager(RssGenerator.class);
		if (rssGenerator != null && rssGenerator.isEnabled()) {
			return this.wikiConfiguration.getBaseURL() + "/" + rssGenerator.getRssFile();
		}

		return null;
	}

	/** {@inheritDoc} */
	@Override
	public String getURL(final String context, String pageName, final String params) {
		if (pageName == null) {
			pageName = this.wikiConfiguration.getFrontPage();
		}
		final URLConstructor urlConstructor = this.getManager(URLConstructor.class);
		return urlConstructor.makeURL(context, pageName, params);
	}

	public void setServletContext(ServletContext context) {
		this.m_servletContext = context;
		/*:FVK: System.err.println("---> setServletContext()");
		StackTraceElement[] st = Thread.currentThread().getStackTrace();
		for (int n = 1; n < 12; n++) {
			if (st.length > n) {
				StackTraceElement sTE = st[n];
				System.err.println("    " + sTE.getClassName() + "::" + sTE.getMethodName() + "()");
			}
		}*/
	}

	/** {@inheritDoc} */
	@Override
	public ServletContext getServletContext() {
		// TODO: этот код устарел (в ElWiki инициализация относительно OSGi, а не сервлета.
		// ServletContext==null).
		return m_servletContext;
	}

	/** {@inheritDoc} */
	@Override
	public String getFinalPageName(final String page) throws ProviderException {
		return this.getManager(CommandResolver.class).getFinalPageName(page);
	}

	/** {@inheritDoc} */
	@Override
	public Charset getContentEncoding() {
		String wiki_encoding = getPreference(GlobalPreferences.Prefs.PROP_ENCODING, String.class);
		return Charset.forName(wiki_encoding);
	}

	/** {@inheritDoc} */
	@Override
	public String getTemplateDir() {
		return getPreference(GlobalPreferences.Prefs.TEMPLATE_DIR, String.class);
	}

	/** {@inheritDoc} */
	@Override
	public String encodeName(String pagename) throws IOException {
		try {
			return URLEncoder.encode(pagename, isUtf8Encoding ? "UTF-8" : "ISO-8859-1");
		} catch (UnsupportedEncodingException e) {
			throw new IOException("ISO-8859-1 not a supported encoding!?!  Your platform is borked.");
		}
	}

	/** {@inheritDoc} */
	@Override
	public String decodeName(String pagerequest) throws IOException {
		try {
			return URLDecoder.decode(pagerequest, isUtf8Encoding ? "UTF-8" : "ISO-8859-1");
		} catch (UnsupportedEncodingException e) {
			throw new IOException("ISO-8859-1 not a supported encoding!?!  Your platform is borked.");
		}
	}

	/** {@inheritDoc} */
	@Override
	public String getRootPath() {
		return m_rootPath;
	}

	/** {@inheritDoc} */
	@Override
	public void setAttribute(final String key, final Object value) {
		m_attributes.put(key, value);
	}

	/** {@inheritDoc} */
	@Override
	@SuppressWarnings("unchecked")
	public <T> T getAttribute(final String key) {
		return (T) m_attributes.get(key);
	}

	/** {@inheritDoc} */
	@Override
	@SuppressWarnings("unchecked")
	public <T> T removeAttribute(final String key) {
		return (T) m_attributes.remove(key);
	}

	@Override
	public IWikiConfiguration getWikiConfiguration() {
		return this.wikiConfiguration;
	}

	////////////////////////////////////////////////////////////////////////////
	/// ? новое для ElWiki :FVK: /// static методы и др.

	/** {@inheritDoc} */
	@Override
	@SuppressWarnings("unchecked")
	@NonNull
	public <T> @NonNull T getManager(Class<T> manager) {
		T result = (T) components.entrySet().stream().filter(e -> manager.isAssignableFrom(e.getKey()))
				.map(Map.Entry::getValue).findFirst().orElse(null);
		return result;
	}

	/** {@inheritDoc} */
	@Override
	@SuppressWarnings("unchecked")
	public <T> List<T> getManagers(Class<T> manager) {
		return (List<T>) components.entrySet().stream().filter(e -> manager.isAssignableFrom(e.getKey()))
				.map(Map.Entry::getValue).collect(Collectors.toList());
	}

}
